package nl.utwente.fmt.vertalerbouw.tarrh.encoder;

import static org.objectweb.asm.Opcodes.AALOAD;
import static org.objectweb.asm.Opcodes.AASTORE;
import static org.objectweb.asm.Opcodes.ACC_FINAL;
import static org.objectweb.asm.Opcodes.ACC_PRIVATE;
import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
import static org.objectweb.asm.Opcodes.ACC_STATIC;
import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC;
import static org.objectweb.asm.Opcodes.ACONST_NULL;
import static org.objectweb.asm.Opcodes.ALOAD;
import static org.objectweb.asm.Opcodes.ANEWARRAY;
import static org.objectweb.asm.Opcodes.ARETURN;
import static org.objectweb.asm.Opcodes.ASTORE;
import static org.objectweb.asm.Opcodes.ATHROW;
import static org.objectweb.asm.Opcodes.DUP;
import static org.objectweb.asm.Opcodes.DUP_X1;
import static org.objectweb.asm.Opcodes.DUP_X2;
import static org.objectweb.asm.Opcodes.GETFIELD;
import static org.objectweb.asm.Opcodes.GETSTATIC;
import static org.objectweb.asm.Opcodes.GOTO;
import static org.objectweb.asm.Opcodes.IADD;
import static org.objectweb.asm.Opcodes.IALOAD;
import static org.objectweb.asm.Opcodes.IAND;
import static org.objectweb.asm.Opcodes.IASTORE;
import static org.objectweb.asm.Opcodes.ICONST_0;
import static org.objectweb.asm.Opcodes.ICONST_1;
import static org.objectweb.asm.Opcodes.IDIV;
import static org.objectweb.asm.Opcodes.IFEQ;
import static org.objectweb.asm.Opcodes.IF_ICMPEQ;
import static org.objectweb.asm.Opcodes.IF_ICMPGE;
import static org.objectweb.asm.Opcodes.IF_ICMPGT;
import static org.objectweb.asm.Opcodes.IF_ICMPLE;
import static org.objectweb.asm.Opcodes.IF_ICMPLT;
import static org.objectweb.asm.Opcodes.IF_ICMPNE;
import static org.objectweb.asm.Opcodes.ILOAD;
import static org.objectweb.asm.Opcodes.IMUL;
import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
import static org.objectweb.asm.Opcodes.INVOKESTATIC;
import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
import static org.objectweb.asm.Opcodes.IOR;
import static org.objectweb.asm.Opcodes.IREM;
import static org.objectweb.asm.Opcodes.IRETURN;
import static org.objectweb.asm.Opcodes.ISTORE;
import static org.objectweb.asm.Opcodes.ISUB;
import static org.objectweb.asm.Opcodes.NEW;
import static org.objectweb.asm.Opcodes.NEWARRAY;
import static org.objectweb.asm.Opcodes.POP;
import static org.objectweb.asm.Opcodes.PUTFIELD;
import static org.objectweb.asm.Opcodes.PUTSTATIC;
import static org.objectweb.asm.Opcodes.RETURN;
import static org.objectweb.asm.Opcodes.SIPUSH;
import static org.objectweb.asm.Opcodes.SWAP;
import static org.objectweb.asm.Opcodes.T_INT;
import static org.objectweb.asm.Opcodes.V1_1;

import java.util.HashMap;
import java.util.Map;

import nl.utwente.fmt.vertalerbouw.tarrh.symboltable.SymbolTable;
import nl.utwente.fmt.vertalerbouw.tarrh.symboltable.SymbolTableException;
import nl.utwente.fmt.vertalerbouw.tarrh.treewalker.TreeWalker;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;

public class EncoderTreeWalker implements
		TreeWalker<EncoderException, IfElseLabels, WhileDoLabels, TryCatchLabels, EncoderClassEntry, Object, Object> {
	private final EncoderClassEntry booleanType = new EncoderClassEntry(null);
	private final String className;
	private EncoderClassEntry currentClass;
	private EncoderFunctionEntry currentFunction = null;
	private ClassWriter cw;
	private ClassWriter gcw;
	private Map<String, byte[]> innerClasses = new HashMap<String, byte[]>();
	private final EncoderClassEntry integerType = new EncoderClassEntry(null);
	private MethodVisitor mw;

	private MethodVisitor mwInit;
	int nextLocalVariableRegister = 0;
	int returnVariableRegister;
	EncoderClassEntry returnVariableType;
	private final EncoderClassEntry stringType = new EncoderClassEntry(null);
	private SymbolTable<EncoderVariableEntry, EncoderFunctionEntry, EncoderClassEntry> symbolTable = new SymbolTable<EncoderVariableEntry, EncoderFunctionEntry, EncoderClassEntry>();
	private final EncoderClassEntry voidType = new EncoderClassEntry(null);

	public EncoderTreeWalker(String className) {
		this.className = className;
	}

	/**
	 * Called after the occurence of a variable.
	 * 
	 * @param identifier
	 *            the identifier of the variable.
	 * @return the resulting type
	 * @throws EncoderException
	 *             if an errors occurs.
	 */
	@Override
	public EncoderClassEntry accessVariable(String identifier) throws EncoderException {
		EncoderVariableEntry entry;
		try {
			entry = symbolTable.retrieveVariable(identifier);
		} catch (SymbolTableException e) {
			throw new EncoderException(e);
		}
		if (entry.isMemberVariable()) {
			mw.visitVarInsn(ALOAD, 0);
			mw.visitFieldInsn(GETFIELD, className + "$" + currentClass.getClassName(), entry.getVariableName(),
					typeToString(entry.getType()));
		} else if (entry.getType().equals(integerType) || entry.getType().equals(booleanType)) {
			mw.visitVarInsn(ILOAD, entry.getRegister());
		} else {
			mw.visitVarInsn(ALOAD, entry.getRegister());
		}
		return entry.getType();
	}

	/**
	 * Called after the occurence of the array index operator.
	 * 
	 * @param expr
	 *            the resulting type from the main expression.
	 * @param index
	 *            the resulting type from the index expression.
	 * @return the resulting type.
	 */
	@Override
	public EncoderClassEntry arrayIndexAccess(EncoderClassEntry expr, EncoderClassEntry index) {
		EncoderClassEntry originalClassEntry = expr.getElementType();
		if (originalClassEntry.equals(integerType) || originalClassEntry.equals(booleanType)) {
			mw.visitInsn(IALOAD);
		} else {
			mw.visitInsn(AALOAD);
		}

		return expr.getElementType();
	}

	/**
	 * Called after the occurence of an array index assignment.
	 * 
	 * @param identifier
	 *            the identifier of the variable.
	 * @param index
	 *            the resulting type from the index expression.
	 * @param expr
	 *            the resulting type from the expression containing the value
	 *            to be assigned.
	 * @return the resulting type.
	 * @throws EncoderException
	 *             if an errors occurs.
	 */
	@Override
	public EncoderClassEntry arrayIndexAssign(String identifier, EncoderClassEntry index, EncoderClassEntry expr)
			throws EncoderException {
		EncoderVariableEntry variable;
		try {
			variable = symbolTable.retrieveVariable(identifier);
		} catch (SymbolTableException e) {
			throw new EncoderException(e);
		}
		mw.visitInsn(DUP_X1);
		if (variable.isMemberVariable()) {
			mw.visitVarInsn(ALOAD, 0);
			mw.visitFieldInsn(GETFIELD, className + "$" + currentClass.getClassName(), variable.getVariableName(),
					typeToString(variable.getType()));
		} else {
			mw.visitVarInsn(ALOAD, variable.getRegister());
		}
		mw.visitInsn(DUP_X2);
		mw.visitInsn(POP);
		if (expr.equals(integerType) || expr.equals(booleanType)) {
			mw.visitInsn(IASTORE);
		} else {
			mw.visitInsn(AASTORE);
		}
		return expr;
	}

	/**
	 * Called after the occurence of an assigment.
	 * 
	 * @param identifier
	 *            the identifier of the variable.
	 * @param expr
	 *            the resulting type from the expression containing the value
	 *            to be assigned.
	 * @return the resulting type.
	 * @throws EncoderException
	 *             if an errors occurs.
	 */
	@Override
	public EncoderClassEntry assign(String identifier, EncoderClassEntry expr) throws EncoderException {
		EncoderVariableEntry variable;
		try {
			variable = symbolTable.retrieveVariable(identifier);
		} catch (SymbolTableException e) {
			throw new EncoderException(e);
		}
		mw.visitInsn(DUP);
		if (variable.isMemberVariable()) {
			mw.visitVarInsn(ALOAD, 0);
			mw.visitInsn(SWAP);
			mw.visitFieldInsn(PUTFIELD, className + "$" + currentClass.getClassName(), variable.getVariableName(),
					typeToString(variable.getType()));
		} else if (variable.getType().equals(integerType) || variable.getType().equals(booleanType)) {
			mw.visitVarInsn(ISTORE, variable.getRegister());
		} else {
			mw.visitVarInsn(ASTORE, variable.getRegister());
		}
		return variable.getType();
	}

	/**
	 * Called at the start of a class declaration.
	 * 
	 * @param className
	 *            the class name of the new class.
	 */
	@Override
	public void classEnd(String className) {
		mwInit.visitInsn(RETURN);
		mwInit.visitMaxs(1, 1);
		mwInit.visitEnd();
		mwInit = null;
		cw.visitEnd();
		innerClasses.put(className, cw.toByteArray());
		cw = null;
		symbolTable.closeScope();
	}

	/**
	 * Called at the end of a class declaration.
	 * 
	 * @param className
	 *            the class name of the new class.
	 * @throws EncoderException
	 *             if an errors occurs.
	 */
	@Override
	public void classStart(String innerClassName) throws EncoderException {
		currentClass = new EncoderClassEntry(innerClassName);
		try {
			symbolTable.enterClassAndOpenScope(innerClassName, currentClass);
		} catch (SymbolTableException e) {
			throw new EncoderException(e);
		}
		String combinedClassName = className + "$" + innerClassName;
		gcw.visitInnerClass(combinedClassName, className, innerClassName, ACC_PRIVATE + ACC_STATIC);
		cw = new ClassWriter(ClassWriter.COMPUTE_MAXS + ClassWriter.COMPUTE_FRAMES);
		cw.visit(V1_1, ACC_PUBLIC, combinedClassName, null, "java/lang/Object", null);

		mw = cw.visitMethod(ACC_SYNTHETIC, "<init>", "(L" + combinedClassName + ";)V", null, null);
		mw.visitVarInsn(ALOAD, 0);
		mw.visitMethodInsn(INVOKESPECIAL, combinedClassName, "<init>", "()V");
		mw.visitInsn(RETURN);
		mw.visitMaxs(1, 2);
		mw.visitEnd();

		mwInit = cw.visitMethod(ACC_PRIVATE, "<init>", "()V", null, null);
		mwInit.visitVarInsn(ALOAD, 0);
		mwInit.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V");
		mw = mwInit;
	}

	/**
	 * Called after the occurence of a local variable declaration with an array
	 * type.
	 * 
	 * @param type
	 *            the type of the elements in the array.
	 * @param identifier
	 *            the identifier of the variable.
	 */
	private void compareOperator(int cmpOp, EncoderClassEntry expr) {
		if (expr.equals(stringType)) {
			mw.visitMethodInsn(INVOKEVIRTUAL, "java/lang/String", "compareTo", "(Ljava/lang/String;)I");
			mw.visitInsn(ICONST_0);
		}

		Label labelTrue = new Label();
		mw.visitJumpInsn(cmpOp, labelTrue);
		mw.visitInsn(ICONST_0);
		Label labelEnd = new Label();
		mw.visitJumpInsn(GOTO, labelEnd);
		mw.visitLabel(labelTrue);
		mw.visitInsn(ICONST_1);
		mw.visitLabel(labelEnd);
	}

	/**
	 * Called after the occurence of a local variable declaration with an array
	 * type.
	 * 
	 * @param type
	 *            the type of the elements in the array.
	 * @param identifier
	 *            the identifier of the variable.
	 * @throws EncoderException
	 *             if an errors occurs.
	 */
	@Override
	public void declareArray(String type, String identifier) throws EncoderException {
		EncoderClassEntry typeEntry;
		try {
			typeEntry = symbolTable.retrieveClass(type);
			symbolTable.enterVariable(identifier, new EncoderVariableEntry(typeEntry.getArrayTypeOfThisClass(),
					nextLocalVariableRegister, false, ""));
		} catch (SymbolTableException e) {
			throw new EncoderException(e);
		}
		nextLocalVariableRegister++;
	}

	/**
	 * Called after the occurence of a local constant declaration.
	 * 
	 * @param type
	 *            the type of the constant value.
	 * @param identifier
	 *            the identifier of the constant.
	 * @param expr
	 *            the resulting type from the expression containing the value
	 *            to be assigned to the constant.
	 * @throws EncoderException
	 *             if an errors occurs.
	 */
	@Override
	public void declareConstant(String type, String identifier, EncoderClassEntry expr) throws EncoderException {
		EncoderClassEntry typeEntry;
		try {
			typeEntry = symbolTable.retrieveClass(type);
			symbolTable.enterVariable(identifier, new EncoderVariableEntry(typeEntry, nextLocalVariableRegister, false,
					""));
		} catch (SymbolTableException e) {
			throw new EncoderException(e);
		}
		if (typeEntry.equals(integerType) || typeEntry.equals(booleanType)) {
			mw.visitVarInsn(ISTORE, nextLocalVariableRegister);
		} else {
			mw.visitVarInsn(ASTORE, nextLocalVariableRegister);
		}
		nextLocalVariableRegister++;
	}

	/**
	 * Called after the occurence of a member variable declaration with an array
	 * type.
	 * 
	 * @param type
	 *            the type of the elements in the array.
	 * @param identifier
	 *            the identifier of the array.
	 * @throws EncoderException
	 *             if an errors occurs.
	 */
	@Override
	public void declareMemberArray(String type, String identifier) throws EncoderException {
		EncoderClassEntry typeEntry;
		EncoderVariableEntry variableEntry;
		try {
			typeEntry = symbolTable.retrieveClass(type);
			variableEntry = new EncoderVariableEntry(typeEntry.getArrayTypeOfThisClass(), -1, true, "v" + identifier);
			symbolTable.enterVariable(identifier, variableEntry);
		} catch (SymbolTableException e) {
			throw new EncoderException(e);
		}
		FieldVisitor fw = cw.visitField(ACC_PRIVATE, variableEntry.getVariableName(), "[" + typeToString(typeEntry),
				null, null);
		fw.visitEnd();
	}

	/**
	 * Called after the occurence of a member constant declaration.
	 * 
	 * @param type
	 *            the type of the constant value.
	 * @param identifier
	 *            the identifier of the constant.
	 * @param expr
	 *            the resulting type from the expression containing the value
	 *            to be assigned to the constant.
	 * @throws EncoderException
	 *             if an errors occurs.
	 */
	@Override
	public void declareMemberConstant(String type, String identifier, EncoderClassEntry expr) throws EncoderException {
		EncoderClassEntry typeEntry;
		EncoderVariableEntry variableEntry;
		try {
			typeEntry = symbolTable.retrieveClass(type);
			variableEntry = new EncoderVariableEntry(typeEntry, -1, true, "c" + identifier);
			symbolTable.enterVariable(identifier, variableEntry);
		} catch (SymbolTableException e) {
			throw new EncoderException(e);
		}
		FieldVisitor fw = cw.visitField(ACC_PRIVATE, variableEntry.getVariableName(), typeToString(typeEntry), null,
				null);
		fw.visitEnd();
		mwInit.visitVarInsn(ALOAD, 0);
		mwInit.visitInsn(SWAP);
		mwInit.visitFieldInsn(PUTFIELD, className + "$" + currentClass.getClassName(), variableEntry.getVariableName(),
				typeToString(typeEntry));
	}

	/**
	 * Called after the occurence of a member variable declaration.
	 * 
	 * @param type
	 *            the type of the variable value.
	 * @param identifier
	 *            the identifier of the variable.
	 * @throws EncoderException
	 *             if an errors occurs.
	 */
	@Override
	public void declareMemberVariable(String type, String identifier) throws EncoderException {
		EncoderClassEntry typeEntry;
		EncoderVariableEntry variableEntry;
		try {
			typeEntry = symbolTable.retrieveClass(type);
			variableEntry = new EncoderVariableEntry(typeEntry, -1, true, "v" + identifier);
			symbolTable.enterVariable(identifier, variableEntry);
		} catch (SymbolTableException e) {
			throw new EncoderException(e);
		}
		FieldVisitor fw = cw.visitField(ACC_PRIVATE, variableEntry.getVariableName(), typeToString(typeEntry), null,
				null);
		fw.visitEnd();
	}

	/**
	 * Called after the occurrence of a local variable declaration.
	 * 
	 * @param type
	 *            the type of the variable value.
	 * @param identifier
	 *            the identifier of the variable.
	 * @throws EncoderException
	 *             if an errors occurs.
	 */
	@Override
	public void declareVariable(String type, String identifier) throws EncoderException {
		EncoderClassEntry typeEntry;
		try {
			typeEntry = symbolTable.retrieveClass(type);
			symbolTable.enterVariable(identifier, new EncoderVariableEntry(typeEntry, nextLocalVariableRegister, false,
					""));
		} catch (SymbolTableException e) {
			throw new EncoderException(e);
		}
		nextLocalVariableRegister++;
	}

	/**
	 * Called after the entire tree is processed.
	 */
	@Override
	public void end() {
		symbolTable.closeScope();
		gcw.visitEnd();
	}

	/**
	 * Called after the occurrence of an expression statement.
	 * 
	 * @param expr
	 *            the resulting type from the expression.
	 */
	@Override
	public void expressionStatement(EncoderClassEntry expr) {
		if (expr.equals(integerType) || expr.equals(booleanType)) {
			mw.visitVarInsn(ISTORE, returnVariableRegister);
		} else if (!expr.equals(voidType)) {
			mw.visitVarInsn(ASTORE, returnVariableRegister);
		}
		returnVariableType = expr;
	}

	/**
	 * Called after the occurrence of an argument in a function declaration.
	 * 
	 * @param position
	 *            the position of the argument, starting at 0 for the first
	 *            argument.
	 * @param type
	 *            the type of argument variable.
	 * @param identifier
	 *            the identifier of the argument variable.
	 * @throws EncoderException
	 *             if an errors occurs.
	 */
	@Override
	public void functionArgument(int position, String type, String identifier) throws EncoderException {
		EncoderClassEntry typeEntry;
		try {
			typeEntry = symbolTable.retrieveClass(type);
			symbolTable.enterVariable(identifier, new EncoderVariableEntry(typeEntry, nextLocalVariableRegister, false,
					""));
		} catch (SymbolTableException e) {
			throw new EncoderException(e);
		}
		nextLocalVariableRegister++;
		currentFunction.addParameterSignature(typeToString(typeEntry));
	}

	/**
	 * Called after the arguments and before the function body in a function
	 * declaration.
	 * 
	 * @param returnType
	 *            the identifier of the function return value.
	 * @param functionName
	 *            the identifier of the function.
	 * @throws EncoderException
	 *             if an errors occurs.
	 */
	@Override
	public void functionBody(String returnType, String functionName) throws EncoderException {
		String desc = "(" + currentFunction.getParameterSignature() + ")"
				+ typeToString(currentFunction.getReturnType());
		if (currentClass == null) {
			mw = gcw.visitMethod(ACC_PUBLIC + ACC_STATIC, currentFunction.getFunctionName(), desc, null,
					new String[] { "java/lang/Exception" });
		} else {
			mw = cw.visitMethod(ACC_PUBLIC, currentFunction.getFunctionName(), desc, null,
					new String[] { "java/lang/Exception" });
		}

		returnVariableRegister = nextLocalVariableRegister;
		nextLocalVariableRegister++;
	}

	/**
	 * Called after the occurrence of an argument in a function call.
	 * 
	 * @param call
	 *            Ignored.
	 * @param position
	 *            the position of the argument, starting at 0 for the first
	 *            argument.
	 * @param expr
	 *            the resulting type from expression containing the value for
	 *            this argument.
	 */
	@Override
	public void functionCallArgument(Object call, int position, EncoderClassEntry expr) {
		// Do nothing.
	}

	/**
	 * Called at the end of a function call.
	 * 
	 * @param call
	 *            Ignored.
	 * @param argumentCount
	 *            the total number of arguments passed to the function.
	 * @param expr
	 *            the resulting type from the expression on which this member
	 *            function is to be executed.
	 * @param functionName
	 *            the identifier of the function to be called.
	 * @return the resulting type.
	 * @throws EncoderException
	 *             if an errors occurs.
	 */
	@Override
	public EncoderClassEntry functionCallEnd(Object call, int argumentCount, EncoderClassEntry expr, String functionName)
			throws EncoderException {
		EncoderFunctionEntry function;
		try {
			function = symbolTable.retrieveMemberFunction(expr, functionName);
		} catch (SymbolTableException e) {
			throw new EncoderException(e);
		}
		String desc = "(" + function.getParameterSignature() + ")" + typeToString(function.getReturnType());
		mw.visitMethodInsn(INVOKEVIRTUAL, className + "$" + expr.getClassName(), function.getFunctionName(), desc);
		return function.getReturnType();
	}

	/**
	 * Called at the end of a function call.
	 * 
	 * @param call
	 *            Ignored.
	 * @param argumentCount
	 *            the total number of arguments passed to the function.
	 * @param functionName
	 *            the identifier of the function to be called.
	 * @return the resulting type.
	 * @throws EncoderException
	 *             if an errors occurs.
	 */
	@Override
	public EncoderClassEntry functionCallEnd(Object call, int argumentCount, String functionName)
			throws EncoderException {
		EncoderFunctionEntry function;
		try {
			function = symbolTable.retrieveFunction(functionName);
		} catch (SymbolTableException e) {
			throw new EncoderException(e);
		}
		;
		String desc = "(" + function.getParameterSignature() + ")" + typeToString(function.getReturnType());
		if (function.isMemberFunction()) {
			mw.visitMethodInsn(INVOKEVIRTUAL, className + "$" + currentClass.getClassName(),
					function.getFunctionName(), desc);
		} else {
			mw.visitMethodInsn(INVOKESTATIC, className, function.getFunctionName(), desc);
		}
		return function.getReturnType();
	}

	/**
	 * Called at the start of a function call.
	 * 
	 * @param expr
	 *            the resulting type from the expression on which this member
	 *            function is to be executed.
	 * @param functionName
	 *            the identifier of the function to be called.
	 * @return null.
	 */
	@Override
	public Object functionCallStart(EncoderClassEntry expr, String functionName) {
		// Do nothing.
		return null;
	}

	/**
	 * Called at the start of a function call.
	 * 
	 * @param functionName
	 *            the identifier of the function to be called.
	 * @return null.
	 * @throws EncoderException
	 *             if an errors occurs.
	 */
	@Override
	public Object functionCallStart(String functionName) throws EncoderException {
		EncoderFunctionEntry function;
		try {
			function = symbolTable.retrieveFunction(functionName);
		} catch (SymbolTableException e) {
			throw new EncoderException(e);
		}
		if (function.isMemberFunction()) {
			mw.visitIntInsn(ALOAD, 0);
		}
		return null;
	}

	/**
	 * Called at the end of a function declaration.
	 * 
	 * @param returnType
	 *            the identifier of the function return value.
	 * @param functionName
	 *            the identifier of the function.
	 */
	@Override
	public void functionEnd(String returnType, String functionName) {
		if (currentFunction.getReturnType().equals(integerType) || currentFunction.getReturnType().equals(booleanType)) {
			mw.visitVarInsn(ILOAD, returnVariableRegister);
			mw.visitInsn(IRETURN);
		} else if (!currentFunction.getReturnType().equals(voidType)) {
			mw.visitVarInsn(ALOAD, returnVariableRegister);
			mw.visitInsn(ARETURN);
		} else {
			mw.visitInsn(RETURN);
		}

		mw.visitMaxs(0, 0);
		mw.visitEnd();
		mw = mwInit;
		currentFunction = null;
		symbolTable.closeScope();
	}

	/**
	 * Called at the start of a function declaration.
	 * 
	 * @param returnType
	 *            the identifier of the function return value.
	 * @param functionName
	 *            the identifier of the function.
	 * @throws EncoderException
	 *             if an errors occurs.
	 */
	@Override
	public void functionStart(String returnType, String functionName) throws EncoderException {
		try {
			currentFunction = new EncoderFunctionEntry(symbolTable.retrieveClass(returnType), "f" + functionName,
					currentClass != null);
			symbolTable.enterFunctionAndOpenScope(functionName, currentFunction);
		} catch (SymbolTableException e) {
			throw new EncoderException(e);
		}
		if (currentClass == null) {
			nextLocalVariableRegister = 0;
		} else {
			nextLocalVariableRegister = 1;
		}
	}

	/**
	 * Called at the start of the else part of an if-else statement.
	 * 
	 * @param ifElse
	 *            the resulting labels from the matching call to ifStart.
	 * @param expr
	 *            the resulting type from expression containing the value for
	 *            the if-else condition.
	 */
	@Override
	public void ifElse(IfElseLabels labels, EncoderClassEntry expr) {
		symbolTable.closeScope();
		symbolTable.openScope();

		mw.visitJumpInsn(GOTO, labels.getEndLabel());
		mw.visitLabel(labels.getElseLabel());
	}

	/**
	 * Called at the end of an if-else statement.
	 * 
	 * @param ifElse
	 *            the resulting labels from the matching call to ifStart.
	 * @param expr
	 *            the resulting type from expression containing the value for
	 *            the if-else condition.
	 */
	@Override
	public void ifEnd(IfElseLabels labels, EncoderClassEntry ex) {
		symbolTable.closeScope();
		mw.visitLabel(labels.getEndLabel());
	}

	/**
	 * Called at the start of an if-else statement.
	 * 
	 * @param expr
	 *            the resulting type from expression containing the value for
	 *            the if-else condition.
	 * @return the labels that will be passed to all calls related to this
	 *         if-else statement.
	 */
	@Override
	public IfElseLabels ifStart(EncoderClassEntry expr) {
		symbolTable.openScope();
		IfElseLabels labels = new IfElseLabels();
		mw.visitJumpInsn(IFEQ, labels.getElseLabel());
		return labels;
	}

	/**
	 * Called at the occurrence of a boolean literal.
	 * 
	 * @param value
	 *            the boolean value of the literal.
	 * @return the resulting type.
	 */
	@Override
	public EncoderClassEntry literalBoolean(boolean value) {
		mw.visitInsn(value ? ICONST_1 : ICONST_0);
		return booleanType;
	}

	/**
	 * Called at the occurrence of an integer literal.
	 * 
	 * @param value
	 *            the integer value of the literal.
	 * @return the resulting type.
	 */
	@Override
	public EncoderClassEntry literalInteger(int i) {
		mw.visitIntInsn(SIPUSH, i);
		return integerType;
	}

	/**
	 * Called at the occurrence of a string literal.
	 * 
	 * @param value
	 *            the string value of the literal.
	 * @return the resulting type.
	 */
	@Override
	public EncoderClassEntry literalString(String value) {
		mw.visitLdcInsn(value);
		return stringType;
	}

	/**
	 * Called at the end of the main function.
	 */
	@Override
	public void mainFunctionEnd() {
		mw.visitInsn(RETURN);
		mw.visitMaxs(0, 0);
		mw.visitEnd();
		mw = null;
		symbolTable.closeScope();
	}

	/**
	 * Called at the start of the main function.
	 */
	@Override
	public void mainFunctionStart() {
		symbolTable.openScope();
		mw = gcw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null,
				new String[] { "java/lang/Exception" });
		returnVariableRegister = 0;
		nextLocalVariableRegister = 1;
	}

	/**
	 * Called at the occurrence of a new array creation.
	 * 
	 * @param type
	 *            the type of the elements of the array.
	 * @param expr
	 *            the resulting type from expression containing the size for
	 *            the new array.
	 * @return the resulting type.
	 * @throws EncoderException
	 *             if an errors occurs.
	 */
	@Override
	public EncoderClassEntry newArray(String type, EncoderClassEntry expr) throws EncoderException {
		EncoderClassEntry classEntry;
		try {
			classEntry = symbolTable.retrieveClass(type);
		} catch (SymbolTableException e) {
			throw new EncoderException(e);
		}
		if (classEntry.equals(integerType) || classEntry.equals(booleanType)) {
			mw.visitIntInsn(NEWARRAY, T_INT);
		} else if (classEntry.equals(stringType)) {
			mw.visitTypeInsn(ANEWARRAY, "java/lang/String");
		} else {
			mw.visitTypeInsn(ANEWARRAY, className + "$" + classEntry.getClassName());
		}

		return classEntry.getArrayTypeOfThisClass();
	}

	/**
	 * Called at the occurrence of a new instance creation.
	 * 
	 * @param type
	 *            the type of the instance
	 * @return the resulting type.
	 * @throws EncoderException
	 *             if an errors occurs.
	 */
	@Override
	public EncoderClassEntry newObject(String type) throws EncoderException {
		EncoderClassEntry classEntry;
		try {
			classEntry = symbolTable.retrieveClass(type);
		} catch (SymbolTableException e) {
			throw new EncoderException(e);
		}

		String combinedClassName = className + "$" + classEntry.getClassName();
		mw.visitTypeInsn(NEW, combinedClassName);
		mw.visitInsn(DUP);
		mw.visitInsn(ACONST_NULL);
		mw.visitMethodInsn(INVOKESPECIAL, combinedClassName, "<init>", "(L" + combinedClassName + ";)V");
		return classEntry;
	}

	/**
	 * Called at the occurrence of the and operator.
	 * 
	 * @param expr1
	 *            the resulting type from left-hand side expression.
	 * @param expr2
	 *            the resulting type from right-hand side expression.
	 * @return the resulting type.
	 */
	@Override
	public EncoderClassEntry operatorAnd(EncoderClassEntry expr1, EncoderClassEntry expr2) {
		mw.visitInsn(IAND);
		return expr1;
	}

	/**
	 * Called at the occurrence of the division operator.
	 * 
	 * @param expr1
	 *            the resulting type from left-hand side expression.
	 * @param expr2
	 *            the resulting type from right-hand side expression.
	 * @return the resulting type.
	 */
	@Override
	public EncoderClassEntry operatorDivision(EncoderClassEntry expr1, EncoderClassEntry expr2) {
		mw.visitInsn(IDIV);
		return expr1;
	}

	/**
	 * Called at the occurrence of the equals operator.
	 * 
	 * @param expr1
	 *            the resulting type from left-hand side expression.
	 * @param expr2
	 *            the resulting type from right-hand side expression.
	 * @return the resulting type.
	 */
	@Override
	public EncoderClassEntry operatorEquals(EncoderClassEntry expr1, EncoderClassEntry expr2) {
		if (expr1 == integerType || expr1 == booleanType) {
			compareOperator(IF_ICMPEQ, expr1);
		} else {
			mw.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Object", "equals", "(Ljava/lang/Object;)Z");
		}
		return booleanType;
	}

	/**
	 * Called at the occurrence of the greater operator.
	 * 
	 * @param expr1
	 *            the resulting type from left-hand side expression.
	 * @param expr2
	 *            the resulting type from right-hand side expression.
	 * @return the resulting type.
	 */
	@Override
	public EncoderClassEntry operatorGreater(EncoderClassEntry expr1, EncoderClassEntry expr2) {
		compareOperator(IF_ICMPGT, expr1);
		return booleanType;
	}

	/**
	 * Called at the occurrence of the greater-or-equals operator.
	 * 
	 * @param expr1
	 *            the resulting type from left-hand side expression.
	 * @param expr2
	 *            the resulting type from right-hand side expression.
	 * @return the resulting type.
	 */
	@Override
	public EncoderClassEntry operatorGreaterEquals(EncoderClassEntry expr1, EncoderClassEntry expr2) {
		compareOperator(IF_ICMPGE, expr1);
		return booleanType;
	}

	/**
	 * Called at the occurrence of the less operator.
	 * 
	 * @param expr1
	 *            the resulting type from left-hand side expression.
	 * @param expr2
	 *            the resulting type from right-hand side expression.
	 * @return the resulting type.
	 */
	@Override
	public EncoderClassEntry operatorLess(EncoderClassEntry expr1, EncoderClassEntry expr2) {
		compareOperator(IF_ICMPLT, expr1);
		return booleanType;
	}

	/**
	 * Called at the occurrence of the less-or-equals operator.
	 * 
	 * @param expr1
	 *            the resulting type from left-hand side expression.
	 * @param expr2
	 *            the resulting type from right-hand side expression.
	 * @return the resulting type.
	 */
	@Override
	public EncoderClassEntry operatorLessEquals(EncoderClassEntry expr1, EncoderClassEntry expr2) {
		compareOperator(IF_ICMPLE, expr1);
		return booleanType;
	}

	/**
	 * Called at the occurrence of the minus operator.
	 * 
	 * @param expr1
	 *            the resulting type from left-hand side expression.
	 * @param expr2
	 *            the resulting type from right-hand side expression.
	 * @return the resulting type.
	 */
	@Override
	public EncoderClassEntry operatorMinus(EncoderClassEntry expr1, EncoderClassEntry expr2) {
		mw.visitInsn(ISUB);
		return expr1;
	}

	/**
	 * Called at the occurrence of the modulo operator.
	 * 
	 * @param expr1
	 *            the resulting type from left-hand side expression.
	 * @param expr2
	 *            the resulting type from right-hand side expression.
	 * @return the resulting type.
	 */
	@Override
	public EncoderClassEntry operatorModulo(EncoderClassEntry expr1, EncoderClassEntry expr2) {
		mw.visitInsn(IREM);
		return expr1;
	}

	/**
	 * Called at the occurrence of the multiply operator.
	 * 
	 * @param expr1
	 *            the resulting type from left-hand side expression.
	 * @param expr2
	 *            the resulting type from right-hand side expression.
	 * @return the resulting type.
	 */
	@Override
	public EncoderClassEntry operatorMultiply(EncoderClassEntry expr1, EncoderClassEntry expr2) {
		mw.visitInsn(IMUL);
		return expr1;
	}

	/**
	 * Called at the occurrence of the not-equals operator.
	 * 
	 * @param expr1
	 *            the resulting type from left-hand side expression.
	 * @param expr2
	 *            the resulting type from right-hand side expression.
	 * @return the resulting type.
	 */
	@Override
	public EncoderClassEntry operatorNotEquals(EncoderClassEntry expr1, EncoderClassEntry expr2) {
		if (expr1 == integerType || expr1 == booleanType) {
			compareOperator(IF_ICMPNE, expr1);
		} else {
			mw.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Object", "equals", "(Ljava/lang/Object;)Z");
			mw.visitInsn(ICONST_0);
			compareOperator(IF_ICMPEQ, integerType);
		}
		return booleanType;
	}

	/**
	 * Called at the occurrence of the or operator.
	 * 
	 * @param expr1
	 *            the resulting type from left-hand side expression.
	 * @param expr2
	 *            the resulting type from right-hand side expression.
	 * @return the resulting type.
	 */
	@Override
	public EncoderClassEntry operatorOr(EncoderClassEntry expr1, EncoderClassEntry expr2) {
		mw.visitInsn(IOR);
		return expr1;
	}

	/**
	 * Called at the occurrence of the plus operator.
	 * 
	 * @param expr1
	 *            the resulting type from left-hand side expression.
	 * @param expr2
	 *            the resulting type from right-hand side expression.
	 * @return the resulting type.
	 */
	@Override
	public EncoderClassEntry operatorPlus(EncoderClassEntry expr1, EncoderClassEntry expr2) {
		if (expr1.equals(integerType)) {
			mw.visitInsn(IADD);
		} else if (expr1.equals(stringType)) {
			mw.visitInsn(SWAP);
			mw.visitTypeInsn(NEW, "java/lang/StringBuilder");
			mw.visitInsn(DUP_X1);
			mw.visitInsn(SWAP);
			mw.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "(Ljava/lang/String;)V");
			mw.visitInsn(SWAP);
			mw.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append",
					"(Ljava/lang/String;)Ljava/lang/StringBuilder;");
			mw.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;");
		}
		return expr1;
	}

	/**
	 * Called at the occurrence of a print statement.
	 * 
	 * @param expr
	 *            the resulting type from expression whose result should be
	 *            printed.
	 */
	@Override
	public void print(EncoderClassEntry expr) {
		mw.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
		mw.visitInsn(SWAP);
		if (expr.equals(stringType)) {
			mw.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V");
		} else if (expr.equals(integerType)) {
			mw.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(I)V");
		} else if (expr.equals(booleanType)) {
			mw.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Z)V");
		}
	}

	/**
	 * Called at the occurrence of a read statement.
	 * 
	 * @param identifier
	 *            the identifier of the variable to which the resulting value
	 *            should be written.
	 * @throws EncoderException
	 *             if an errors occurs.
	 */
	@Override
	public void read(String identifier) throws EncoderException {
		EncoderVariableEntry variable;
		try {
			variable = symbolTable.retrieveVariable(identifier);
		} catch (SymbolTableException e) {
			throw new EncoderException(e);
		}
		mw.visitFieldInsn(GETSTATIC, className, "in", "Ljava/io/BufferedReader;");
		mw.visitMethodInsn(INVOKEVIRTUAL, "java/io/BufferedReader", "readLine", "()Ljava/lang/String;");
		if (variable.getType().equals(integerType)) {
			mw.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "parseInt", "(Ljava/lang/String;)I");
			mw.visitVarInsn(ISTORE, variable.getRegister());
		} else if (variable.getType().equals(booleanType)) {
			mw.visitMethodInsn(INVOKESTATIC, "java/lang/Boolean", "parseBoolean", "(Ljava/lang/String;)Z");
			mw.visitVarInsn(ISTORE, variable.getRegister());
		} else {
			mw.visitVarInsn(ASTORE, variable.getRegister());
		}
	}

	/**
	 * Called at the start of the tree.
	 * 
	 * @throws EncoderException
	 *             if an errors occurs.
	 */
	@Override
	public void start() throws EncoderException {
		gcw = new ClassWriter(ClassWriter.COMPUTE_MAXS + ClassWriter.COMPUTE_FRAMES);
		gcw.visit(V1_1, ACC_PUBLIC, className, null, "java/lang/Object", null);

		MethodVisitor mw = gcw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
		mw.visitVarInsn(ALOAD, 0);
		mw.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V");
		mw.visitInsn(RETURN);
		mw.visitMaxs(1, 1);
		mw.visitEnd();

		FieldVisitor fw = gcw.visitField(ACC_PRIVATE + ACC_FINAL + ACC_STATIC, "in", "Ljava/io/BufferedReader;", null,
				null);
		fw.visitEnd();
		mw = gcw.visitMethod(ACC_STATIC, "<clinit>", "()V", null, null);
		mw.visitTypeInsn(NEW, "java/io/BufferedReader");
		mw.visitInsn(DUP);
		mw.visitTypeInsn(NEW, "java/io/InputStreamReader");
		mw.visitInsn(DUP);
		mw.visitFieldInsn(GETSTATIC, "java/lang/System", "in", "Ljava/io/InputStream;");
		mw.visitMethodInsn(INVOKESPECIAL, "java/io/InputStreamReader", "<init>", "(Ljava/io/InputStream;)V");
		mw.visitMethodInsn(INVOKESPECIAL, "java/io/BufferedReader", "<init>", "(Ljava/io/Reader;)V");
		mw.visitFieldInsn(PUTSTATIC, className, "in", "Ljava/io/BufferedReader;");
		mw.visitInsn(RETURN);
		mw.visitMaxs(5, 0);
		mw.visitEnd();

		symbolTable.openScope();
		try {
			symbolTable.enterClassAndOpenScope("Integer", integerType);
			symbolTable.closeScope();
			symbolTable.enterClassAndOpenScope("String", stringType);
			symbolTable.closeScope();
			symbolTable.enterClassAndOpenScope("Boolean", booleanType);
			symbolTable.closeScope();
			symbolTable.enterClassAndOpenScope("Void", voidType);
			symbolTable.closeScope();
		} catch (SymbolTableException e) {
			throw new EncoderException(e);
		}
	}

	/**
	 * Called at the end of a statement expression.
	 * 
	 * @param statementExpression
	 *            Ignored.
	 * @return the resulting expression object.
	 */
	@Override
	public EncoderClassEntry statementExpressionEnd(Object statementExpression) {
		symbolTable.closeScope();
		if (returnVariableType.equals(integerType) || returnVariableType.equals(booleanType)) {
			mw.visitIntInsn(ILOAD, returnVariableRegister);
		} else {
			mw.visitIntInsn(ALOAD, returnVariableRegister);
		}
		return returnVariableType;
	}

	/**
	 * Called at the start of a statement expression.
	 * 
	 * @return null.
	 */
	@Override
	public Object statementExpressionStart() {
		symbolTable.openScope();
		return null;
	}

	/**
	 * Called at the occurrence of a throw statement.
	 * 
	 * @param expr
	 *            the resulting type from expression whose result should be
	 *            thrown
	 * @throws EncoderException
	 *             if an errors occurs.
	 */
	@Override
	public void throwException(EncoderClassEntry expr) throws EncoderException {
		mw.visitTypeInsn(NEW, "java/lang/Exception");
		mw.visitInsn(DUP_X1);
		mw.visitInsn(SWAP);
		mw.visitMethodInsn(INVOKESPECIAL, "java/lang/Exception", "<init>", "(Ljava/lang/String;)V");
		mw.visitInsn(ATHROW);
	}

	/**
	 * Get the bytecode generated by this encoder.
	 * 
	 * @return the bytecode generated by this encoder.
	 */
	public Bytecode toBytecode() {
		return new Bytecode(className, gcw.toByteArray(), innerClasses);
	}

	/**
	 * Called at the end of the catch part of a try-catch statement.
	 * 
	 * @param tryCatch
	 *            the resulting labels from the matching call to tryStart.
	 * @param type
	 *            the type of the catch variable.
	 * @param identifier
	 *            the identifier of the catch variable.
	 */
	@Override
	public void tryCatchEnd(TryCatchLabels labels, String type, String identifier) {
		mw.visitLabel(labels.getEndLabel());
		symbolTable.closeScope();
	}

	/**
	 * Called at the start of the catch part of a try-catch statement.
	 * 
	 * @param tryCatch
	 *            the resulting labels from the matching call to tryStart.
	 * @param type
	 *            the type of the catch variable.
	 * @param identifier
	 *            the identifier of the catch variable.
	 * @throws EncoderException
	 *             if an errors occurs.
	 */
	@Override
	public void tryCatchStart(TryCatchLabels labels, String type, String identifier) throws EncoderException {
		mw.visitLabel(labels.getBeforeCatchLabel());
		symbolTable.openScope();
		try {
			symbolTable.enterVariable(identifier, new EncoderVariableEntry(stringType, nextLocalVariableRegister,
					false, ""));
		} catch (SymbolTableException e) {
			throw new EncoderException(e);
		}
		mw.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Exception", "getMessage", "()Ljava/lang/String;");
		mw.visitVarInsn(ASTORE, nextLocalVariableRegister);
		nextLocalVariableRegister++;

	}

	/**
	 * Called at the end of the try part of a try-catch statement.
	 * 
	 * @param tryCatch
	 *            the resulting labels from the matching call to tryStart.
	 */
	@Override
	public void tryEnd(TryCatchLabels labels) {
		mw.visitLabel(labels.getAfterTryLabel());
		mw.visitJumpInsn(GOTO, labels.getEndLabel());
		symbolTable.closeScope();
	}

	/**
	 * Called at the start of the try part of a try-catch statement.
	 * 
	 * @return labels that will be passed to all calls related to this
	 *         try-catch statement.
	 */
	@Override
	public TryCatchLabels tryStart() {
		TryCatchLabels labels = new TryCatchLabels();
		mw.visitTryCatchBlock(labels.getBeforeTryLabel(), labels.getAfterTryLabel(), labels.getBeforeCatchLabel(),
				"java/lang/Exception");
		mw.visitLabel(labels.getBeforeTryLabel());
		symbolTable.openScope();
		return labels;
	}

	/**
	 * Convert a class entry to the matching JVM signature string.
	 * 
	 * @param type
	 *            the class entry to convert.
	 * @return the matching JVM signature string.
	 */
	private String typeToString(EncoderClassEntry type) {
		if (type.equals(integerType) || type.equals(booleanType)) {
			return "I";
		} else if (type.equals(voidType)) {
			return "V";
		} else if (type.equals(stringType)) {
			return "Ljava/lang/String;";
		} else if (type.isArrayType()) {
			return "[" + typeToString(type.getElementType());
		} else {
			return "L" + className + "$" + type.getClassName() + ";";
		}
	}

	/**
	 * Called after the condition of a while-do statement.
	 * 
	 * @param whileDo
	 *            the resulting labels from the matching call to whileStart.
	 * @param expr
	 *            the resulting type from condition expression.
	 * @throws EncoderException
	 *             if an errors occurs.
	 */
	@Override
	public void whileCondition(WhileDoLabels labels, EncoderClassEntry expr) throws EncoderException {
		symbolTable.openScope();
		mw.visitJumpInsn(IFEQ, labels.getEndLabel());
	}

	/**
	 * Called at the end of a while-do statement.
	 * 
	 * @param whileDo
	 *            the resulting labels from the matching call to whileStart.
	 * @param expr
	 *            the resulting type from condition expression.
	 */
	@Override
	public void whileEnd(WhileDoLabels labels, EncoderClassEntry expr) {
		symbolTable.closeScope();
		mw.visitJumpInsn(GOTO, labels.getConditionLabel());
		mw.visitLabel(labels.getEndLabel());
	}

	/**
	 * Called at the start of a while-do statement.
	 * 
	 * @return labels that will be passed to all calls related to this
	 *         while-do statement.
	 * @throws EncoderException
	 *             if an errors occurs.
	 */
	@Override
	public WhileDoLabels whileStart() throws EncoderException {
		WhileDoLabels labels = new WhileDoLabels();
		mw.visitLabel(labels.getConditionLabel());
		return labels;
	}
}
